<?php

require_once "Zend/Tokenizer/Interface.php";
require_once 'Zend/Socket/Holder/Interface.php';

/**
 * @category   Zend
 * @package    Zend_Socket
 * @copyright  Salomon BRYS
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 *
 * This class simply encapsulate the PHP Socket calls.
 *
 * This class does exactly what the system does
 * except that it bufferizes the calls, adds the states and the protections.
 */
class Zend_Socket implements Zend_Tokenizer_Interface, Zend_Socket_Holder_Interface
{
	/**
	 * Constants that defines the state of the socket
	 */
	const STATE_CREATED   = 'created';
	const STATE_BINDED    = 'binded';
	const STATE_CONNECTED = 'connected';
	const STATE_LISTENING = 'listening';
	const STATE_CLOSED    = 'closed';

	/**
	 * Constants that protect the socket use
	 */
	const PROT_READ      = 1; // 001
	const PROT_WRITE     = 2; // 010
	const PROT_READWRITE = 3; // 011
	const PROT_LISTEN    = 4; // 100

	/**
	 * Max len of read.
	 */
	const READ_LEN = 8192;

	/**
	 * The state of the socket
	 *
	 * @var string
	 */
	protected $_state = self::STATE_CREATED;

	/**
	 * The protection flags of the socket
	 *
	 * @var int
	 */
	protected $_protection = 0; // No authorization

	/**
	 * The socket
	 *
	 * @var resource
	 */
	protected $_resource = null;

	/**
	 * The read buffer.
	 * Stores what has been read and not yet retrived with getToken.
	 *
	 * @var string|array[string]string
	 */
	protected $_readBuffer = '';

	/**
	 * The write buffer.
	 * Used when a write has not completely succeeded.
	 *
	 * @var string|array[string]string
	 */
	protected $_writeBuffer = '';

	/**
	 * The write buffer.
	 * Used when a write has not completely succeeded.
	 *
	 * @var array[]Zend_Socket
	 */
	protected $_acceptBuffer = array();

	/**
	 * The type of the socket.
	 * Stores to access it only once form the PHP socket API.
	 *
	 * @var int
	 */
	protected $_socketType = 0;

	/**
	 * Creates a Zend_Socket_Abstract from a PHP Socket.
	 * Should not be called directly but by Zend_Socket_*_Socket
	 *
	 * @var int      $protection
	 * @var resource $socket
	 */
	public function __construct($protection, $resource)
	{
		$this->_protection = $protection;
		$this->_resource = $resource;
		$this->_socketType = socket_get_option($resource, SOL_SOCKET, SO_TYPE);

		if ($protection & self::PROT_LISTEN) {
			if (($protection & self::PROT_READ) || ($protection & self::PROT_WRITE)) {
				require_once 'Zend/Socket/Exception.php';
				throw new Zend_Socket_Exception(
					"Cannot set PROT_READ or PROT_WRITE on a socket with PROT_LISTEN",
					$this
				);
			}
			if ($this->_socketType != SOCK_STREAM) {
				require_once 'Zend/Socket/Exception.php';
				throw new Zend_Socket_Exception(
					"Socket with PROT_LISTEN need to be SOCK_STREAM.",
					$this
				);
			}
		}
		if ($this->_socketType != SOCK_STREAM && $this->_socketType != SOCK_DGRAM) {
			require_once 'Zend/Socket/Exception.php';
			throw new Zend_Socket_Exception(
				"Only SOCK_STREAM and SOCK_DGRAM sockets are supported by Zend_Socket.",
				$this
			);
		}

		$this->_state = self::STATE_CREATED;
	}

	/**
	 * Closes the socket if it has not be closed yet
	 */
	public function __destruct()
	{
		if ($this->_state != self::STATE_CLOSED) {
			$this->close();
		}
	}

	/**
	 * Bind the socket.
	 * No specific protection needed.
	 * The socket must be in the STATE_CREATED state.
	 *
	 * @var string $address
	 * @var int $port
	 * @return bool
	 */
	public function bind($address, $port)
	{
		if ($this->_state != self::STATE_CREATED) {
			require_once 'Zend/Socket/Exception.php';
			throw new Zend_Socket_Exception(
				"To bind, the socket must be in the state STATE_CREATED.",
				$this
			);
		}
		if ($this->_socketType == SOCK_DGRAM) {
			$this->_readBuffer = array();
			$this->_writeBuffer = array();
		}
		$ret = @socket_bind($this->_resource, $address, $port);
		$this->_state = self::STATE_BINDED;
		return $ret;
	}

	/**
	 * Connect the socket.
	 * No specific protection needed.
	 * The socket must be in the STATE_BINDED state.
	 * The socket must be a SOCK_STREAM type.
	 *
	 * @var string $address
	 * @var int $port
	 * @return bool
	 */
	public function connect($address, $port)
	{
		if ($this->_state != self::STATE_CREATED) {
			require_once 'Zend/Socket/Exception.php';
			throw new Zend_Socket_Exception(
				"To connect, the socket must be in the state STATE_CREATED.",
				$this
			);
		}
		$ret = @socket_connect($this->_resource, $address, $port);
		$this->_state = self::STATE_CONNECTED;
		return $ret;
	}

	/**
	 * Tell the socket to listen for new connections.
	 * Need PROT_LISTEN protection.
	 * The socket needs to be a SOCK_STREAM type (like TCP).
	 * The socket must be in the STATE_BINDED state.
	 *
	 * @var int $backlog
	 * @return bool
	 */
	public function listen($backlog = 0)
	{
		if (!($this->_protection & self::PROT_LISTEN)) {
			require_once 'Zend/Socket/Exception.php';
			throw new Zend_Socket_Exception(
				"This socket does not have PROT_LISTEN protection and therefore cannot listen.",
				$this
			);
		}
		if ($this->_state != self::STATE_BINDED) {
			require_once 'Zend/Socket/Exception.php';
			throw new Zend_Socket_Exception(
				"To listen, the socket must be in the state STATE_BINDED.",
				$this
			);
		}
		$ret = @socket_listen($this->_resource, $backlog);
		$this->_state = self::STATE_LISTENING;
		return $ret;
	}

	/**
	 * Accepts a new connection.
	 * Need PROT_LISTEN protection.
	 * The socket needs to be a SOCK_STREAM type (like TCP).
	 * The socket must be in the STATE_LISTENING state.
	 * The new connection is pushed on the accept buffer, to be retrieved by getAccept.
	 *
	 * @var in $protection The protection of the new socket.
	 * @return int (0 or 1, to be used by readOrAccept)
	 */
	public function accept($protection = self::PROT_READWRITE)
	{
		if ($this->_state != self::STATE_LISTENING) {
			require_once 'Zend/Socket/Exception.php';
			throw new Zend_Socket_Exception(
				"To listen, the socket must be in the state STATE_LISTENING.",
				$this
			);
		}
		$res = socket_accept($this->_resource);
		if ($res) {
			$newSocket = new Zend_Socket($protection, $res);
			$newSocket->_state = self::STATE_CONNECTED;
			array_push($this->_acceptBuffer, $newSocket);
			return 1;
		}
		return 0;
	}

	/**
	 * Get the next Accepted connection.
	 *
	 * @var bool $acceptFirst Wether to call accept() first.
	 * @var in $protection The protection of the new socket if $acceptFirst is true.

	 * @return Zend_Socket
	 */
	public function getAccept($acceptFirst = false, $protection = self::PROT_READWRITE)
	{
		if ($acceptFirst) {
			$this->accept($protection);
		}
		if (empty($this->_acceptBuffer))
			return null;
		return array_shift($this->_acceptBuffer);
	}

	public function countPendingAccepted()
	{
		return count($this->_acceptBuffer);
	}

	/**
	 * Read some data on the socket and stores it in the read buffer.
	 * The result can be retrieved using the getRead function.
	 * Need PROT_READ protection.
	 * The socket must be in the STATE_BINDED state if is a SOCK_STREAM or  type (like TCP).
	 * The socket must be in the STATE_CREATED state if is a SOCK_DGRAM type (like UDP).
	 * Return the len of what was read.
	 *
	 * @return int
	 */
	public function read()
	{
		if (!($this->_protection & self::PROT_READ)) {
			require_once 'Zend/Socket/Exception.php';
			throw new Zend_Socket_Exception(
				"This socket does not have PROT_READ protection and therefore cannot read.",
				$this
			);
		}
		$this->_checkRWState();
		$read = '';
		if ($this->_state == self::STATE_CONNECTED) {
			$read = socket_read($this->_resource, self::READ_LEN);
			$this->_readBuffer .= $read;
		} else {
			$addr = '';
			$port = 0;
			socket_recvfrom($this->_resource, $read, self::READ_LEN, 0, $addr, $port);
			$name = $addr.':'.$port;
			if (!isset($this->_readBuffer[$name])) {
				$this->_readBuffer[$name] = '';
			}
			$this->_readBuffer[$name] .= $read;
		}
		return strlen((binary)$read);
	}

	/**
	 * Return the current size of the read buffer.
	 *
	 * @return int
	 */
	public function readLen()
	{
		if (is_array($this->_readBuffer)) {
			return strlen((binary)implode('', $this->_readBuffer));
		}
		return strlen((binary)$this->_readBuffer);
	}

	/**
	 * Alias used to be compatible with Zend_Tokenizer
	 */
	public function hasToken()
	{
		if ($this->_state == self::STATE_LISTENING) {
			return !empty($this->_acceptBuffer);
		}
		return !empty($this->_readBuffer);
	}

	/**
	 * Get everything the socket has read until the last clean.
	 *
	 * @param bool $cleanBuffer If the buffer should be clean.
	 * @param bool $readBefore If the read function should be called first.
	 * @throws Zend_Socket_Exception
	 */
	public function getRead($cleanBuffer = false, $readBefore = false)
	{
		if (!($this->_protection & self::PROT_READ)) {
			require_once 'Zend/Socket/Exception.php';
			throw new Zend_Socket_Exception(
				"This socket does not have PROT_READ protection and therefore cannot read.",
				$this
			);
		}
		if ($readBefore) {
			$this->read();
		}
		$ret = $this->_readBuffer;
		if ($cleanBuffer == true) {
			if (is_array($this->_readBuffer)) {
				$this->_readBuffer = array();
			} else {
				$this->_readBuffer = '';
			}
		}
		return $ret;
	}

	/**
	 * Alias used to be compatible with Zend_Tokenizer
	 */
	public function getRawData()
	{
		if ($this->_state == self::STATE_LISTENING) {
			return $this->_acceptBuffer;
		}
		return $this->getRead();
	}

	/**
	 * Alias used to be compatible with Zend_Tokenizer
	 */
	public function getToken()
	{
		if ($this->_state == self::STATE_LISTENING) {
			return $this->getAccept();
		}
		return $this->getRead(true);
	}

	/**
	 * Call read() or accept().
	 * To be used after a call to Zend_Select_Socket when this socket has been marked as readable.
	 *
	 * @param int $protection
	 * @return int
	 */
	public function readOrAccept($protection = self::PROT_READWRITE)
	{
		if ($this->_state == self::STATE_LISTENING) {
			return $this->accept($protection);
		}
		return $this->read();
	}

	/**
	 * Add some data to the write buffer.
	 * This data will then be effectively be send when calling write()
	 * Need PROT_WRITE protection.
	 *
	 * @var string $data
	 * @var string $toAddr can be addr OR addr:port
	 * @var int $port
	 * @return void
	 */
	public function writeLater($data, $toAddr = '', $toPort = 0)
	{
		if (!($this->_protection & self::PROT_WRITE)) {
			require_once 'Zend/Socket/Exception.php';
			throw new Zend_Socket_Exception(
				"This socket does not have PROT_WRITE protection and therefore cannot write.",
				$this
			);
		}
		if (is_array($this->_writeBuffer)) {
			if (strpos($toAddr, ':') !== false && $toPort == 0) {
				list($toAddr, $toPort) = explode(':', $toAddr, 2);
			}
			if ($toAddr == '' || $toPort == 0) {
				require_once 'Zend/Socket/Exception.php';
				throw new Zend_Socket_Exception(
					"This socket needs an Addr and a Port to write to.",
					$this
				);
			}
			$this->_writeBuffer[$toAddr . ':' . $toPort] = $data;
		} else {
			$this->_writeBuffer .= $data;
		}
	}

	/**
	 * Return the current size of the read buffer.
	 *
	 * @return int
	 */
	public function writeLen()
	{
		if (is_array($this->_writeBuffer)) {
			return strlen((binary)implode('', $this->_writeBuffer));
		}
		return strlen((binary)$this->_writeBuffer);
	}

	/**
	 * Write the buffer data on the socket.
	 * Return wether all data has been written (true)
	 * or if another call to this method is needed to end the data write (false).
	 * If a data is given, add it at the end of the buffer before writing the buffer.
	 * Need PROT_WRITE protection.
	 * The socket must be in the STATE_BINDED state.
	 *
	 * @var string $data
	 * @return bool
	 */
	public function write($data = false, $toAddr = '', $toPort = 0)
	{
		if (!($this->_protection & self::PROT_WRITE)) {
			require_once 'Zend/Socket/Exception.php';
			throw new Zend_Socket_Exception(
				"This socket does not have PROT_WRITE protection and therefore cannot write.",
				$this
			);
		}
		$this->_checkRWState();
		if (!empty($data)) {
			$this->writeLater($data, $toAddr, $toPort);
		}
		if (is_array($this->_writeBuffer)) {
			foreach ($this->_writeBuffer as $name => &$text) {
				list($addr, $port) = explode(':', $name);
				$len = socket_sendto($this->_resource, $text, strlen((binary)$text), 0, $addr, $port);
				$text = substr($text, $len);
				if (empty($text)) {
					unset($this->_writeBuffer[$name]);
				}
			}
		} else {
			$len = socket_write($this->_resource, $this->_writeBuffer);
			$this->_writeBuffer = substr($this->_writeBuffer, $len);
			return empty($this->_writeBuffer);
		}
	}

	/**
	 * Close the socket.
	 * The buffers can be accessed but the socket cannot be used anymore.
	 *
	 * @return void
	 */
	public function close()
	{
		if ($this->_state == self::STATE_CLOSED) {
			require_once 'Zend/Socket/Exception.php';
			throw new Zend_Socket_Exception(
				"The socket has already been closed.",
				$this
			);
		}
		socket_close($this->_resource);
		$this->_state = self::STATE_CLOSED;
	}

	/**
	 * Alias used to be compatible with Zend_Tokenizer
	 */
	public function isEndOfStream()
	{
		return $this->_state == self::STATE_CLOSED;
	}

	/**
	 * @return string
	 */
	public function getState()
	{
		return $this->_state;
	}

	/**
	 * @return int
	 */
	public function getProtection()
	{
		return $this->_protection;
	}

	/**
	 * @return ressource
	 */
	public function getSocketRessource()
	{
		return $this->_resource;
	}

	/**
	 * @return string
	 */
	public function getSockName()
	{
		$name = '';
		$port = 0;
		@socket_getsockname($this->_resource, $name, $port);
		return $name . ':' . $port;
	}

	/**
	 * @return string
	 */
	public function getPeerName()
	{
		$name = '';
		$port = 0;
		@socket_getpeername($this->_resource, $name, $port);
		return $name . ':' . $port;
	}

	/**
	 * @return string
	 */
	public function getFullName()
	{
		return '<' . $this->getSockName() . '>' . $this->getPeerName();
	}

	/**
	 * @param bool $clear
	 * @return array
	 */
	public function getLastError($clear = false)
	{
		$code = socket_last_error($this->_resource);
		if ($clear) {
			socket_clear_error($this->_resource);
		}
		return array(
			'code' => $code,
			'message' => socket_strerror($code)
		);
	}

	/**
	 * Return wether this socket produces string tokens
	 *
	 * @return bool
	 */
	public function isStringToken()
	{
		if ($this->_state == self::STATE_CONNECTED) {
			return true;
		}
		return false;
	}

	/**
	 * Get the PHP resource socket.
	 * For internal use only.
	 *
	 * @return resource
	 */
	public function getResource()
	{
		return $this->_resource;
	}

	/**
	 * Alias used to be compatible with Zend_Tokenizer
	 */
	public function getProducer()
	{
		return $this;
	}

	/**
	 * Check if the socket is in the right state and has the correct rights to read or write.
	 * Throws an exception in case it can't.
	 *
	 * @throws Zend_Socket_Exception
	 * @return void
	 */
	private function _checkRWState()
	{
		if ($this->_socketType == SOCK_STREAM && $this->_state != self::STATE_CONNECTED) {
			require_once 'Zend/Socket/Exception.php';
			throw new Zend_Socket_Exception(
				"To read or write, this socket must be in the state STATE_CONNECTED.",
				$this
			);
		} elseif ($this->_socketType == SOCK_DGRAM && $this->_state != self::STATE_CONNECTED && $this->_state != self::STATE_BINDED) {
			require_once 'Zend/Socket/Exception.php';
			throw new Zend_Socket_Exception(
				"To read or write, this socket must be in the state STATE_CONNECTED or STATE_BINDED.",
				$this
			);
		}
	}

	/**
	 * Alias for Zend_Socket_Holder_Interface.
	 *
	 * @return Zend_Socket
	 */
	public function Socket()
	{
		return $this;
	}

}
